package com.fub.positioning;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
import android.content.Context;
import android.util.Log;

import com.fub.guidance.Building;
import com.fub.guidance.Point;
import com.fub.util.PreferenceDAO;

public class FingerprintService {

	private static int UPDATE_INTERVALL_IN_MS = 1500;
	private static int MEASUREMENT_INTERVALL_IN_MS = 100;
	public static int NUMBER_MEASUREMENTS_FOR_POSITIONING = 10;
	public static int NUMBER_MEASUREMENTS_FOR_REFERENCEPOINT = 50;
	public static int K = 3;
	private PriorityQueue<ReferencePoint> nearestNeighbours;
	private List<ReferencePoint> referencePoints;
	private List<Measurement> recordedMeasurements;
	private Point kNearestNeighbour;
	private Point kWeightedNearestNeighbour;
	private boolean stopThread;
	private IWifiProvider wifiProvider;
	private Context context;
	private PreferenceDAO preferenceDAO;
	private static FingerprintService instance;
	private boolean isRecording;
	private boolean isMeasureRP;
	public boolean isMeasureDistance;
	public LinkedList<Measurement> measurements;

	/**
	 * Constructs a new fingerprint service
	 * 
	 * @param context
	 *            the application context
	 */
	private FingerprintService(Context context) {
		this.context = context;
		this.nearestNeighbours = new PriorityQueue<ReferencePoint>(20,
				new ReferencePointComperator());
		this.measurements = new LinkedList<Measurement>();
		this.referencePoints = new ArrayList<ReferencePoint>();
		this.wifiProvider = new WifiProvider(context);
		this.preferenceDAO = new PreferenceDAO(context);
		this.recordedMeasurements = new ArrayList<Measurement>();
	}

	/**
	 * Returns an instance of this class.
	 * 
	 * @param context
	 *            the application context
	 * @return an instance of this class
	 */
	public static FingerprintService getInstance(Context context) {
		if (instance == null) {
			instance = new FingerprintService(context);
		}
		return instance;
	}

	/**
	 * Starts a thread that measures the signal strength to all access points in
	 * a update interval.
	 */
	public void startMeasuringThread() {
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				while (!FingerprintService.this.stopThread) {
					synchronized (this) {
						Measurement measurement = measureAndReturnAverage(NUMBER_MEASUREMENTS_FOR_POSITIONING);
						if (measurement != null) {
							FingerprintService.this.measurements
									.add(measurement);
							Measurement avererageMeasure = PositioningUtil
									.getAverageFromManyMeasurements(FingerprintService.this.measurements);
							if (FingerprintService.this.measurements.size() >= 5) {
								FingerprintService.this.measurements.poll();
							}
							updateNearestNeighbours(avererageMeasure);
							calculateKnnAndKwnn();
						}
						PositioningUtil.sleep(UPDATE_INTERVALL_IN_MS);
					}
				}
			}
		};
		new Thread(runnable).start();
	}

	public Point doSingleMeasurementAndReturnKWNNPosition() {
		Measurement measurement = measureAndReturnAverage(NUMBER_MEASUREMENTS_FOR_POSITIONING);
		if (measurement != null) {
			updateNearestNeighbours(measurement);
			calculateKnnAndKwnn();
		}
		return this.kWeightedNearestNeighbour;
	}

	/**
	 * Resets the instance.
	 */
	public void reset() {
		this.nearestNeighbours = new PriorityQueue<ReferencePoint>(20,
				new ReferencePointComperator());
		this.referencePoints = new ArrayList<ReferencePoint>();
		this.wifiProvider = new WifiProvider(this.context);
		this.preferenceDAO = new PreferenceDAO(this.context);
		this.recordedMeasurements = new ArrayList<Measurement>();
		this.isMeasureRP = false;
	}

	/**
	 * Returns all reference points.
	 * 
	 * @return all reference points.
	 */
	public List<ReferencePoint> getReferencePoints() {
		return this.referencePoints;
	}

	/**
	 * * Returns a specific reference point.
	 * 
	 * @param location
	 *            the location of the desired reference point
	 * @return a specific reference point
	 */
	public ReferencePoint getReferencePoint(String location) {
		for (ReferencePoint rp : this.referencePoints) {
			if (location.equals(rp.getLocation())) {
				return rp;
			}
		}
		return null;
	}

	/**
	 * Returns a list of reference points, sorted by the signal distance to the
	 * last measurement.
	 * 
	 * @return a list of reference points
	 */
	public ReferencePoint[] getNearestNeigbours() {
		synchronized (this.nearestNeighbours) {
			return this.nearestNeighbours
					.toArray(new ReferencePoint[this.nearestNeighbours.size()]);
		}
	}

	/**
	 * Returns the calculated KNN point.
	 * 
	 * @return the calculated KNN point.
	 */
	public Point getKNearestNeighbour() {
		return this.kNearestNeighbour;
	}

	/**
	 * Returns the calculated KWNN point.
	 * 
	 * @return the calculated KWNN point.
	 */
	public Point getKWeightedNearestNeighbour() {
		return this.kWeightedNearestNeighbour;
	}

	/**
	 * Returns a list of recorded measurements.
	 * 
	 * @return a list of recorded measurements.
	 */
	public List<Measurement> getRecordedMeasurements() {
		return this.recordedMeasurements;
	}

	/**
	 * Returns true, if the measuring thread has been stopped.
	 * 
	 * @return true, if the measuring thread has been stopped.
	 */
	public boolean isStopThread() {
		return this.stopThread;
	}

	/**
	 * Stops the thread that measures the signal strength to all access points
	 */
	public void disableMeasuringThread() {
		this.stopThread = true;
	}

	/**
	 * Enables the thread that measures the signal strength to all access points
	 */
	public void enableMeasuringThread() {
		this.stopThread = false;
	}

	/**
	 * Adds a new reference points an measure signal strength to all access
	 * points.
	 * 
	 * @param buildingID
	 *            the id of the building
	 * @param position
	 *            the position of the new reference point
	 */
	public void addReferencePoint(String buildingID, Point position) {
		synchronized (this.referencePoints) {
			Measurement averageMeasurement = measureAndReturnAverage(NUMBER_MEASUREMENTS_FOR_REFERENCEPOINT);
			Log.d("Map", "Füge neuen Referenzpunkt hinzu");
			this.referencePoints.add(new ReferencePoint(buildingID,
					PositioningUtil.createLocationLabel(buildingID, position),
					position, averageMeasurement));
		}
	}

	/**
	 * Returns true, if the mode measureAp is activated.
	 * 
	 * @return true, if the mode measureAp is activated
	 */
	public boolean isMeasureRP() {
		return this.isMeasureRP;
	}

	/**
	 * Enables or disables the mode measureAp.
	 * 
	 * @param isMeasureRP
	 *            true, to enable the mode measureAp
	 */
	public void setMeasureRP(boolean isMeasureRP) {
		this.isMeasureRP = isMeasureRP;
	}

	/**
	 * Creates evenly distributed reference points.
	 * 
	 * @param distance
	 *            the distance of the reference points to each other
	 */
	public void createEvenSpreadReferencePoints(int distance) {

		Building building = Building.getInstance();
		float maxX = building.getMaxX();
		float maxY = building.getMaxX();

		for (int x = 180; x < maxX; x += distance) {
			for (int y = 100; y < maxY; y += distance) {
				Point position = new Point(x, y,
						this.preferenceDAO.getCurrentEtage());
				ReferencePoint referencePoint = new ReferencePoint(
						this.preferenceDAO.getBuildingId(),
						PositioningUtil.createLocationLabel(
								this.preferenceDAO.getBuildingId(), position),
						position, new Measurement());
				this.referencePoints.add(referencePoint);
			}
		}
	}

	/**
	 * Replace all reference points.
	 * 
	 * @param newReferencePoints
	 *            the new reference points
	 */
	public void replaceReferencePoints(List<ReferencePoint> newReferencePoints) {
		this.referencePoints = newReferencePoints;
	}

	public void removeReferencePoint(String buildingId, Point position) {
		for (int i = 0; i < this.referencePoints.size(); i++) {
			ReferencePoint referencePoint = this.referencePoints.get(i);
			if (referencePoint.getLocation().equals(
					PositioningUtil.createLocationLabel(buildingId, position))) {
				this.referencePoints.remove(referencePoint);
			}
		}
	}

	/**
	 * Measures the signal strength to all access points and returns the average
	 * value.
	 * 
	 * @param times
	 *            how many measurements should be done
	 * @return
	 */
	public Measurement measureAndReturnAverage(int times) {
		synchronized (this.wifiProvider) {
			List<Measurement> measurements = new ArrayList<Measurement>();
			for (int i = 0; i < times; i++) {
				Measurement measurement = this.wifiProvider
						.measureAccessPointsSignalStrength();
				if (measurement == null) {
					return null;
				}
				measurements.add(measurement);
				PositioningUtil.sleep(MEASUREMENT_INTERVALL_IN_MS);
			}
			if (this.isRecording) {
				this.recordedMeasurements.addAll(measurements);
			}
			return PositioningUtil.getAverageFromManyMeasurements(measurements);
		}
	}

	/**
	 * Returns true, if the the fingerprint service is recording measurements.
	 * 
	 * @return ture, if the the fingerprint service is recording measurements
	 */
	public boolean isRecording() {
		return this.isRecording;
	}

	/**
	 * Starts the recording of measurements.
	 */
	public void startRecording() {
		this.recordedMeasurements.clear();
		this.isRecording = true;
	}

	/**
	 * Stops the recording of measurements.
	 */
	public void stopRecording() {
		this.isRecording = false;
	}

	/**
	 * Plays the recorded measurements.
	 */
	public void startPlayback() {
		synchronized (this.wifiProvider) {
			this.wifiProvider = new PlaybackWifiProvider(this.context);
		}
	}

	/**
	 * Stops the recorded measurements.
	 */
	public void stopPlayback() {
		this.wifiProvider = new WifiProvider(this.context);
	}

	/**
	 * Updates the nearest neighbor list by calculating the signal distance of
	 * every reference point.
	 * 
	 * @param currentMeasurement
	 *            the last measurement
	 */
	private void updateNearestNeighbours(Measurement currentMeasurement) {
		synchronized (this.nearestNeighbours) {
			Log.d("Map", "Update Nearest Neighbours");
			this.nearestNeighbours.clear();
			for (int i = 0; i < this.referencePoints.size(); i++) {
				ReferencePoint referencePoint = this.referencePoints.get(i);
				referencePoint.setDistanceToCurrentPosition(PositioningUtil
						.getManhattenDistance(currentMeasurement,
								referencePoint.getMeasurement()));
				this.nearestNeighbours.add(referencePoint);
			}
		}
	}

	private void calculateKnnAndKwnn() {
		List<ReferencePoint> referencePoints = new ArrayList<ReferencePoint>(
				this.nearestNeighbours);

		List<ReferencePoint> bestKReferencePoints = new ArrayList<ReferencePoint>();
		List<ReferencePoint> bestKReferencePointsForKNN = new ArrayList<ReferencePoint>();
		for (int i = 0; i < K; i++) {
			if (referencePoints.size() >= K) {
				bestKReferencePoints.add(referencePoints.get(i));
				bestKReferencePointsForKNN.add(referencePoints.get(i));
			}
		}
		int[] posKwnn = PositioningUtil.calculateKWNNPosition(
				bestKReferencePoints, K);
		int[] posKnn = PositioningUtil.calculateKNNPoint(
				bestKReferencePointsForKNN, K);

		this.kWeightedNearestNeighbour = new Point(posKwnn[0], posKwnn[1],
				this.preferenceDAO.getCurrentEtage());
		this.kNearestNeighbour = new Point(posKnn[0], posKnn[1],
				this.preferenceDAO.getCurrentEtage());
	}
}
